package com.adamfass.tractorbeam.gameupdatelogic;

import java.util.Set;

import com.adamfass.tractorbeam.Blocker;
import com.adamfass.tractorbeam.Controller;
import com.adamfass.tractorbeam.GameUpdateLogic;
import com.adamfass.tractorbeam.PhysicsGameObject;
import com.jme.intersection.BoundingPickResults;
import com.jme.math.FastMath;
import com.jme.math.Matrix3f;
import com.jme.math.Ray;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.shape.Cylinder;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.MaterialState;
import com.jme.scene.state.MaterialState.MaterialFace;
import com.jme.system.DisplaySystem;

public class TractorBeamGameUpdateLogic implements GameUpdateLogic {
  private static final float START_DISTANCE = 100;
  public static final float PEAK_DISTANCE = 200;
  public static final float END_DISTANCE = 500;
  private static final float PEAK_STRENGTH = 9000;
   
  private final Node rootNode;
  private final Controller controller;
  private final TractorBeamTargeter targeter;
  private final PhysicsGameObject shooter;
  private final Set<Blocker> blockerSet;
  private final Set<TractorBeamRecord> tractorBeamRecordSet;
  
  private boolean tractorBeamIsOn = false;
  private PhysicsGameObject target;
  private TractorBeamRecord tractorBeamRecord;
  
  private boolean beamGeometryAttached = false;
  private Node beamGeometry;
  
  public TractorBeamGameUpdateLogic(
      Node rootNode,
      Controller controller,
      PhysicsGameObject shooter,
      TractorBeamTargeter targeter,
      Set<Blocker> blockerSet,
      Set<TractorBeamRecord> tractorBeamRecordSet) {
    this.rootNode = rootNode;
    this.controller = controller;
    this.targeter = targeter;
    this.shooter = shooter;
    this.blockerSet = blockerSet;
    this.tractorBeamRecordSet = tractorBeamRecordSet;
    
    beamGeometry = new Node();
    final Cylinder innerBeam = new Cylinder("inner beam", 2, 10, 2, 1);
    final MaterialState innerBeamMaterialState = 
      DisplaySystem.getDisplaySystem().getRenderer().createMaterialState();
    innerBeamMaterialState.setEnabled(true);
    innerBeamMaterialState.setDiffuse(new ColorRGBA(0, 0, 0, 0.25f));
    innerBeamMaterialState.setAmbient(new ColorRGBA(1, 1, 0, 0.25f));
    innerBeamMaterialState.setEmissive(new ColorRGBA(1, 1, 0, 0.25f));
    innerBeamMaterialState.setSpecular(new ColorRGBA(0, 0, 0, 0.25f));
    innerBeamMaterialState.setMaterialFace(MaterialFace.FrontAndBack);
    innerBeam.setRenderState(innerBeamMaterialState);
    beamGeometry.attachChild(innerBeam);
    
    final Cylinder outerBeam = new Cylinder("outer beam", 2, 10, 8, 1);
    final MaterialState outerBeamMaterialState = 
      DisplaySystem.getDisplaySystem().getRenderer().createMaterialState();
    outerBeamMaterialState.setEnabled(true);
    outerBeamMaterialState.setDiffuse(new ColorRGBA(0, 0, 0, 0.1f));
    outerBeamMaterialState.setAmbient(new ColorRGBA(0, 1, 0, 0.1f));
    outerBeamMaterialState.setEmissive(new ColorRGBA(0, 1, 0, 0.1f));
    outerBeamMaterialState.setSpecular(new ColorRGBA(0, 0, 0, 0.1f));
    outerBeamMaterialState.setMaterialFace(MaterialFace.FrontAndBack);
    outerBeam.setRenderState(outerBeamMaterialState);
    beamGeometry.attachChild(outerBeam);
    
    final BlendState beamBlendState = 
      DisplaySystem.getDisplaySystem().getRenderer().createBlendState();
    beamBlendState.setBlendEnabled(true);
    beamBlendState.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
    beamBlendState.setDestinationFunction(
        BlendState.DestinationFunction.OneMinusSourceAlpha);
    beamBlendState.setTestEnabled(true);
    beamBlendState.setTestFunction(BlendState.TestFunction.GreaterThan);
    beamBlendState.setEnabled(true);
    beamGeometry.setRenderState(beamBlendState);
    
    beamGeometry.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT);
  }
  
  public void update(float timePerFrame) {
    if (tractorBeamIsOn && controller.isTractorBeamOn()) {
      if (target.isDeleted()) {
        // Do nothing until the button is released.
        hideTractorBeamGeometry();
        hideBlockedFeedbackGeometry();
      } else {
        showTractorBeamAndApplyForcesOrShowBlocked();
      }
    } else if (tractorBeamIsOn && !controller.isTractorBeamOn()) {
      tractorBeamIsOn = false;
      hideTractorBeamGeometry();
      hideBlockedFeedbackGeometry();
      targeter.update();
      targeter.showTargetingFeedback();
      tractorBeamRecordSet.remove(tractorBeamRecord);
    } else if (!tractorBeamIsOn && controller.isTractorBeamOn()) {
      targeter.update();
      target = targeter.getTarget();
      if (target != null) {
        tractorBeamIsOn = true;
        targeter.hideTargetingFeedback();
        showTractorBeamAndApplyForcesOrShowBlocked();
        
        tractorBeamRecord = new TractorBeamRecord(shooter, target);
        tractorBeamRecordSet.add(tractorBeamRecord);
      } else {
        targeter.showTargetingFeedback();
      }
    } else if (!tractorBeamIsOn && !controller.isTractorBeamOn()) {
      targeter.showTargetingFeedback();
      targeter.update();
    }
  }
  
  public void showTractorBeamAndApplyForcesOrShowBlocked() {
    if (isTractorBeamBlocked()) {
      hideTractorBeamGeometry();
      showAndUpdateBlockedFeedbackGeometry();
    } else {
      showAndUpdateTractorBeamGeometry();
      hideBlockedFeedbackGeometry();
      applyTractorBeamForces();
    }
  }
  
  private void showAndUpdateTractorBeamGeometry() {
    if (!beamGeometryAttached) {
      beamGeometryAttached = true;
      rootNode.attachChild(beamGeometry);
    }
    
    final Vector3f shooterToTarget = target.getPosition().subtract(
        shooter.getPosition());
    final float beamLength = shooterToTarget.length();
    final float beamForceMagnitude = beamForceMagnitude(beamLength);
    final float normalizedMagnitude = beamForceMagnitude / PEAK_STRENGTH;
    beamGeometry.setLocalScale(
        new Vector3f(normalizedMagnitude, normalizedMagnitude, beamLength));
    
    final Matrix3f rotation = new Matrix3f();
    rotation.fromStartEndVectors(Vector3f.UNIT_Z, shooterToTarget.normalize());
    beamGeometry.setLocalRotation(rotation);
    
    final Vector3f translation = new Vector3f();
    translation.interpolate(
        shooter.getPosition(), target.getPosition(), 0.5f);
    beamGeometry.setLocalTranslation(translation);
    
    beamGeometry.updateGeometricState(0, true);
  }
  
  private void hideTractorBeamGeometry() {
    if (beamGeometryAttached) {
      beamGeometryAttached = false;
      beamGeometry.removeFromParent();
    }
  }
  
  private void showAndUpdateBlockedFeedbackGeometry() {
    // TODO(afass): implement
  }
  
  private void hideBlockedFeedbackGeometry() {
    // TODO(afass): implement
  }
  
  private static float gently(float x, float a, float b) {
    if (x < a) {
      return ((b - 1)/(a *  (b * b - a * b + a - 1))) * x * x;
    } else if (x > b) {
      float a3 = 1 / (b * b - a * b +  a - 1);
      float b3 = -2 * a3;
      float c3 = 1 + a3;
      return a3 * x * x + b3 * x + c3;
    } else {
      float m  = 2 * (b - 1) / (b * b - a * b + a - 1);
      float b2 = -m * a / 2;
      return m * x + b2;
    }
  }
  
  private static float beamForceMagnitude(float distance) {
    if (distance < START_DISTANCE) {
      return 0;
    } else if (distance == PEAK_DISTANCE) {
      return PEAK_STRENGTH;
    } else if (distance < PEAK_DISTANCE) {
      float t = (distance - START_DISTANCE) / (PEAK_DISTANCE - START_DISTANCE);
      float a = 1 / 3;
      float b = 2 / 3;
      return PEAK_STRENGTH * gently(t, a, b);
    } else if (distance < END_DISTANCE) {
      float t = 1 - ((distance - PEAK_DISTANCE) / (END_DISTANCE - PEAK_DISTANCE));
      float a = 8 / 10;
      float b = 9 / 10;
      return PEAK_STRENGTH * gently(t, a, b);
    } else {
      return 0;
    }
  }
  
  private boolean isTractorBeamBlocked() {
    final Vector3f shooterToTarget = target.getPosition().subtract(
        shooter.getPosition());
    final float maxDistance = shooterToTarget.length();
    final Ray ray = 
      new Ray(shooter.getPosition(), shooterToTarget.normalize());
    
    for (Blocker blocker : blockerSet) {
      if (blocker == shooter || blocker == target) {
        continue;
      }
      final BoundingPickResults pickResults = new BoundingPickResults();
      pickResults.setCheckDistance(true);
      blocker.getBlockerSpatial().findPick(ray, pickResults);
      if (pickResults.getNumber() > 0 
          && FastMath.abs(pickResults.getPickData(0).getDistance()) < maxDistance) {
        return true;
      }
    }
    return false;
  }
  
  private void applyTractorBeamForces() {
    final Vector3f shooterToTarget = target.getPosition().subtract(
        shooter.getPosition());
    final float beamForceMagnitude = beamForceMagnitude(shooterToTarget.length());
    final Vector3f beamForce = 
      shooterToTarget.normalize().multLocal(beamForceMagnitude);
    shooter.addForce(beamForce);
    target.addForce(beamForce.multLocal(-1));
  }
}
